package com.growthgate.bootcommon.handler;

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.growthgate.bootcommon.annotation.ApiIgnoreFields;
import com.growthgate.bootcommon.annotation.ApiShowFields;
import com.growthgate.bootcommon.util.StringUtils;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiParam;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.StringMemberValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.AllowableListValues;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * API参数处理器
 *
 * @author GrowthGate
 * @date 2020/2/20
 */
@Order
@Component
public class ApiParameterHandler implements ParameterBuilderPlugin {

    @Resource
    private TypeResolver typeResolver;

    @Override
    public void apply(@NotNull ParameterContext context) {
        ResolvedMethodParameter methodParameter = context.resolvedMethodParameter();
        Class<?> erasedType = methodParameter.getParameterType().getErasedType();
        // 处理对象的显示字段和排除字段
        Optional<ApiIgnoreFields> apiIgnoreFields = methodParameter.findAnnotation(ApiIgnoreFields.class).toJavaUtil();
        Optional<ApiShowFields> apiShowFields = methodParameter.findAnnotation(ApiShowFields.class).toJavaUtil();
        Annotation annotation = null;
        if (apiShowFields.isPresent()) {
            annotation = apiShowFields.get();
        } else if (apiIgnoreFields.isPresent()) {
            annotation = apiIgnoreFields.get();
        }
        if (annotation != null) {
            // model名称
            String name = erasedType.getSimpleName() + "$" + StringUtils.randomUuid();
            // 向documentContext的Models中添加新生成的Class
            context.getDocumentationContext().getAdditionalModels()
                    .add(typeResolver.resolve(createRefModel(annotation, erasedType.getPackage() + "." + name, erasedType)));
            // 修改model参数的ModelRef为我们动态生成的class
            context.parameterBuilder().parameterType("body").modelRef(new ModelRef(name));
        }
        // 处理枚举类
        if (erasedType.isEnum()) {
            // 生成枚举描述
            StringBuilder description = new StringBuilder();
            // 获取原描述
            Optional<ApiParam> apiParam = methodParameter.findAnnotation(ApiParam.class).toJavaUtil();
            apiParam.ifPresent(param -> description.append(param.value()).append('\n'));
            // 获取枚举值
            Enum<?>[] enums = (Enum<?>[]) erasedType.getEnumConstants();
            List<String> ordinals = Arrays.stream(enums).map(e -> {
                description.append(e.ordinal()).append(": ").append(e.toString()).append('\n');
                return Integer.toString(e.ordinal());
            }).collect(Collectors.toList());
            AllowableListValues allowableListValues = new AllowableListValues(ordinals, erasedType.getTypeName());
            // 固定设置为int类型
            ResolvedType resolvedType = typeResolver.resolve(int.class);
            context.parameterBuilder().description(description.toString()).allowableValues(allowableListValues).type(resolvedType);
        }
    }

    @Nullable
    private Class<?> createRefModel(Annotation annotation, String name, Class<?> originClass) {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.makeClass(name);
        try {
            Field[] fields = originClass.getDeclaredFields();
            List<Field> fieldList = Arrays.asList(fields);
            List<Field> dealFields = Collections.emptyList();
            if (annotation instanceof ApiIgnoreFields) {
                ApiIgnoreFields apiIgnoreFields = (ApiIgnoreFields) annotation;
                List<String> ignoreProperties = Arrays.asList(apiIgnoreFields.value());
                dealFields = fieldList.stream().filter(s -> !ignoreProperties.contains(s.getName()) && !"serialVersionUID".equals(s.getName()))
                        .collect(Collectors.toList());
            } else if (annotation instanceof ApiShowFields) {
                ApiShowFields apiShowFields = (ApiShowFields) annotation;
                List<String> ignoreProperties = Arrays.asList(apiShowFields.value());
                dealFields = fieldList.stream().filter(s -> ignoreProperties.contains(s.getName())).collect(Collectors.toList());
            }
            for (Field field : dealFields) {
                CtField ctField = new CtField(ClassPool.getDefault().get(field.getType().getName()), field.getName(), ctClass);
                ctField.setModifiers(Modifier.PUBLIC);
                ApiModelProperty ampAnno = originClass.getDeclaredField(field.getName()).getAnnotation(ApiModelProperty.class);
                String attributes = Optional.ofNullable(ampAnno).map(ApiModelProperty::value).orElse("");
                // 添加model属性说明
                if (StringUtils.isNotBlank(attributes)) {
                    ConstPool constPool = ctClass.getClassFile().getConstPool();
                    AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
                    javassist.bytecode.annotation.Annotation ann = new javassist.bytecode.annotation.Annotation(ApiModelProperty.class.getName(), constPool);
                    ann.addMemberValue("value", new StringMemberValue(attributes, constPool));
                    attr.addAnnotation(ann);
                    ctField.getFieldInfo().addAttribute(attr);
                }
                ctClass.addField(ctField);
            }
            return ctClass.toClass();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public boolean supports(DocumentationType documentationType) {
        return true;
    }

}